"""
-----声明-----

本代码为河北雄安素水互联网科技有限公司编写
联系人：王建存，电话：18510412016

打印输出内容级别：
①无前缀，为常规打印
②##前缀，为关键点内容输出
③@@前缀，为功能成功输出
④^^前缀，为错误或故障输出

---------------
"""

import system
import modbus as modbus_engine
from modem import info as modemInfo
from modem import sim
from driver import GPIO

import ubinascii
import utime
import ujson

import http
import hota
import sntp
import network

import _thread

from number_toolkit import number_toolkit
from modbus_toolkit import modbus_toolkit
from umqtt_simple import MQTTClient
from prj_info_board import prj_info_board

soft_ver = "2025-0312-1050"
prj_info = {}

led_STA = GPIO()
led_NET = GPIO()
# hunger_dog = WDT()
# print("launch dog: ", hunger_dog.open("hunger_dog"))

# 机器信息
machine_info = {}
"""
sys_ver = "xaiot_rd_12ju_20240816"
imei = ""
imsi = ""
dev_product_id = ""
dev_model = ""
firm_ver = ""
"""
# 联网状态
g_connect_status = False
sntp_time_sync_date = []
http_client = http.client()

simple_water_server = "https://www.sushui.wang/"

mqtt_connected = 0
mqtt_connect_state = -1
if_looping = False

mqtt_client = None
heartBeat = 120
qos = 1
mqtt_con_flag = False
pingresp_rcv_flag = False
lock = True
next_ping_time = 0
PING_INTERVAL = 120

publish_ts = 0


def check_machine():
    global machine_info

    imei = modemInfo.getDevImei()
    imsi = sim.getImsi()
    dev_product_id = modemInfo.getDevProductId()
    dev_model = modemInfo.getDevModel()
    firm_ver = modemInfo.getDevFwVersion()
    print("check machine:")
    print("device IMEI:", imei)
    print("device IMSI:", imsi)
    print("device model:", dev_model)
    print("dev product id:", dev_product_id)
    print("device firmware version", firm_ver)


    machine_info = {
        "imei": imei,
        "if_checked_ota": False,
        "imsi": imsi,
        "dev_model": dev_model,
        "dev_product_id": dev_product_id,
        "firm_ver": firm_ver,
        "soft_ver": soft_ver,
    }
    second_count = int(machine_info["imei"]) % 86400
    hour_index = int(second_count / 3600)
    second_count = second_count % 3600
    minute_index = int(second_count / 60)
    machine_info["imei_ota_time"] = [hour_index, minute_index]


def on_4g_cb(args):
    global g_connect_status
    pdp = args[0]
    netwk_sta = args[1]
    if netwk_sta == 1:
        g_connect_status = True
    else:
        g_connect_status = False


def connect_network(try_times):
    print("start to connect cell network")
    net = network.NetWorkClient()
    g_register_network = False
    if net._stagecode is not None and net._stagecode == 3 and net._subcode == 1:
        g_register_network = True
    else:
        g_register_network = False

    if g_register_network:
        net.on(1, on_4g_cb)
        net.connect(None)
        print("network register successed net.connect(None)")
        # haas_iokit.func_ledFlash(led_NET, "led_NET", "fast", 10)

        if sntp.settime() == 1:
            global sntp_time_sync_date
            sntp_time_sync_date = [
                now_local_time[0],
                now_local_time[1],
                now_local_time[2],
            ]
            print("@@ getting sntp time succ")
        else:
            print("^^ getting sntp time fail")

        return g_register_network

    else:
        print("network register failed(g_register_network fail)")
        # haas_iokit.func_ledFlash(led_NET, "led_NET", "slow", 5)
        utime.sleep_ms(500)
        # haas_iokit.func_ledFlash(led_NET, "led_NET", "slow", 5)
        utime.sleep_ms(500)

        if try_times > 0:
            connect_network(try_times - 1)
        else:
            print("*** unable to connect net, reboot now..")
            utime.sleep(120)
            system.reset()


def encode_ts(ts):
    # uhashlib.hmacsha1("simplewater",bytes)
    str_ts = str(ts)
    encoded_str = ""
    while len(str_ts):
        encoded_str + str_ts[-1]
        str_ts = str_ts[:-1]
    return encoded_str


def http_post_req(url, body):

    http_body = ujson.dumps(body)

    header = "Content-Type: application/json\r\nAccept: application/json\r\n"
    try:
        htp = http.client()
        htp.set_header(header)
        htp.set_data(http_body, len(http_body))
        ret = htp.post(url)
        if ret < 0:
            print("http post return failed %d" % ret)
            return None
        responsecode = htp.get_response_code()

        if responsecode == 200:
            print("https success")
            response = htp.get_response()

            try:
                print(
                    "response",
                    response,
                )
                response = ujson.loads(response)
                return response
            except:
                print(
                    "loads error ",
                )

                return {}

        else:
            print("https fail")
            return {}
    except:
        print("***ERROR*** http_post error (line 155) ")

        return {}


def check_mqtt_valid():
    pass


def get_prj_info(try_times):
    global prj_info
    prj_info_demo = {
        "mqtt_params": {
            "client_id": "",
            "mqtt_url": "",
            "mqtt_port": "",
            "product_key": "",
            "password": "",
            "keepalive": "",
            "pub_period": 2,
        },
        "http_params": {
            "http_url": "",
            "access_token": "",
        },
        "rs_232": {"com_board_item": "rs485_9600", "device_list": [{}]},
        "rs_485": {},
        "di": {},
        "do": {},
        "relay": {},
        "tf": {},
    }
    # 本地读取
    if (
        # 云端
        (
            "mqtt_params" in prj_info_board
            and "mqtt_url" in prj_info_board["mqtt_params"]
            and len(prj_info_board["mqtt_params"]["mqtt_url"])
        )
        or (
            "http_params" in prj_info_board
            and "http_url" in prj_info_board["http_params"]
            and len(prj_info_board["http_params"]["http_url"])
        )
        # 设备端
        and (
            (
                "rs_232" in prj_info_board
                and "com_board_item" in prj_info_board["rs_232"]
                and len(prj_info_board["rs_232"]["com_board_item"])
            )
            or (
                "rs_485" in prj_info_board
                and "com_board_item" in prj_info_board["rs_485"]
                and len(prj_info_board["rs_485"]["com_board_item"])
            )
            or (
                "di" in prj_info_board
                and "device_list" in prj_info_board["di"]
                and len(prj_info_board["di"]["device_list"])
            )
            or (
                "do" in prj_info_board
                and "device_list" in prj_info_board["do"]
                and len(prj_info_board["do"]["device_list"])
            )
            or (
                "relay" in prj_info_board
                and "device_list" in prj_info_board["relay"]
                and len(prj_info_board["relay"]["device_list"])
            )
        )
    ):
        # 确认了本地有配置文件
        print("local board prj_info acquired.")
        prj_info = prj_info_board
        prj_info["if_get_prj_info"] = True
        return True

    # 若本地没有，则需要云端获取
    localtime = utime.localtime()
    ts = utime.mktime(localtime)
    encoded_str = encode_ts(ts)
    print("ori ts", ts, "encoded: ", encoded_str)

    print("*** start check if ota ***")

    post_body = {
        # 完全是硬件信息，不包括任何工程信息，最多增加本代码版本信息
        "action_type": "get_haas_prj_info",
        "time": "{}-{}-{}_{}:{}:{}".format(
            localtime[0],
            localtime[1],
            localtime[2],
            localtime[3],
            localtime[4],
            localtime[5],
        ),
        "ts": ts,
        "imei": machine_info["imei"],
        # "imsi": machine_info["imsi"],
        "manu": machine_info["dev_model"] + "#" + machine_info["firm_ver"],
        "product": machine_info["dev_product_id"],
        "sys_ver": machine_info["sys_ver"],
        "soft_ver": machine_info["soft_ver"],
    }

    http_ret = http_post_req(simple_water_server, post_body)

    if "prj_info" in http_ret:
        print("get succ")
        prj_info = http_ret["prj_info"]
        prj_info["if_get_prj_info"] = True
        return True
    else:
        if try_times > 0:
            get_prj_info(try_times - 1)
        else:
            print("*** unable to get prj_info, reboot now..")
            utime.sleep(120)
            system.reset()


def check_ota():
    ota_time = utime.localtime()
    print("*** start check if ota ***")
    global machine_info
    launch_body = {
        "time": "{}-{}-{} {}:{}:{}".format(
            ota_time[0],
            ota_time[1],
            ota_time[2],
            ota_time[3],
            ota_time[4],
            ota_time[5],
        ),
        # 世纪初的秒+世纪后的秒
        "ts": utime.mktime(ota_time),
        "imei": machine_info["imei"],
        "imsi": machine_info["imsi"],
        "manu": machine_info["dev_model"] + "#" + machine_info["firm_ver"],
        "product": machine_info["dev_product_id"],
        "project": "xaiot",
        "softver": machine_info["sys_ver"],
    }
    launch_body = ujson.dumps(launch_body)

    header = "Content-Type: application/json\r\nAccept: application/json\r\n"
    try:
        htp = http.client()
        htp.set_header(header)
        htp.set_data(launch_body, len(launch_body))
        ret = htp.post(simple_water_server)
        if ret < 0:
            print("http post return failed %d" % ret)
            return None
        responsecode = htp.get_response_code()

        if responsecode == 200:
            print("https success")
            response = htp.get_response()

            try:
                print(
                    "response",
                    response,
                )
                response = ujson.loads(response)
                if response["code"] == 200:
                    zip_url = response["file_list"][0]["url"]

                    hot = hota.client()
                    if hot.fotaPy(zip_url) == 0:
                        print("---------OTA---------")
                        # 马上更新下ota检查状态
                        machine_info["if_checked_ota"] = False
                        utime.sleep(1)
                        system.reset()
                    else:
                        print("^^ ota failed!")
            except:
                print(
                    "loads error ",
                )

            utime.sleep(3)
            return None
        else:
            print("https fail")
            return None
    except:
        print("***ERROR*** http_post error (line 155) ")
        return -1


def sub_cb(topic, msg):
    msg = str(ubinascii.hexlify(msg))

    if len(msg) != 25:
        # print("call back msg:", msg)
        return

    print("######### sub_cb: " "msg: ", msg, len(msg))

    sub_topic = topic.decode()
    print("sub_cb: " "topic:", sub_topic)
    if sub_topic.find("down") < 0:
        print("^^ This is not down topic")
        return

    sub_topic = sub_topic.replace("down", "down_reply")
    # sub_topic += "/accepted"
    print("sub_topic resp: ", sub_topic)

    if len(msg) != 25:
        print("^^ msg len error", len(msg))
        return
    msg = msg[2:-1]
    msg_id = msg[2:10]
    func_code = msg[10:12]
    prop_code = msg[12:16]
    prop_value = msg[16:18]

    if func_code == "02":
        # 继电器操作

        # 获取功能ID字符串prop_code对应的十进制
        prop_code = int(prop_code, 16)
        prop_value = int(prop_value)
        prop_value_reverse = int(not (prop_value))

        print("prop_code: ", prop_code, "prop_value: ", prop_value)
        for device in prj_info["mqtt_params"]["devices"]:
            # 查找
            if (
                device["index"]
                == prop_code
                # and device["com_params"]["func"] == "modbus_05_writeSingleCoils"
            ):
                print("find the subject: ", device["subject"])
                if_write_success = False

                if device["com_params"]["io_type"] in ["485", "232"]:
                    # 判断是haas板载的还是rs接口
                    # 要写入，而不是读取
                    modbus_engine.init(device["com_params"]["com_board_item"])
                    device["com_params"]["value_to_set"] = prop_value
                    result_value = getattr(
                        modbus_toolkit(), device["com_params"]["func_title"]
                    )(
                        modbus_engine,
                        device["com_params"],
                        5,
                    )

                    # 执行成功
                    if result_value:
                        # 判断是否持久型hold的，不是点按
                        if not device["com_params"]["if_hold"]:
                            utime.sleep_ms(100)
                            device["com_params"]["value_to_set"] = prop_value_reverse
                            modbus_result = getattr(
                                modbus_toolkit(), device["com_params"]["func_title"]
                            )(
                                modbus_engine,
                                device["com_params"],
                                5,
                            )
                            if modbus_result:
                                if_write_success = True
                                print(" un hold succ")
                            else:
                                print("un hold error")
                        else:
                            print("no need to unhold")
                            if_write_success = True

                    else:
                        print("bool set error")

                    modbus_engine.deinit()
                elif device["com_params"]["io_type"] == "haas_relay":
                    # gpio action
                    the_gpio = GPIO()
                    the_gpio.open(device["com_params"]["com_board_item"])
                    the_gpio.write(prop_value)
                    utime.sleep_ms(100)
                    gpio_value = the_gpio.read()
                    print("gpio value", gpio_value, prop_value)
                    if gpio_value == prop_value:
                        if not device["com_params"]["if_hold"]:
                            the_gpio.write(prop_value_reverse)
                            utime.sleep_ms(100)
                            if gpio_value == prop_value_reverse:
                                if_write_success = True
                                print("gpio unhold success", prop_value_reverse)
                            else:
                                print("gpio unhold failed", prop_value_reverse)
                        else:
                            print("no need to unhold")
                            if_write_success = True
                    else:
                        print("gpio set failed", prop_value)
                    utime.sleep(2)
                    the_gpio.close()

                    if if_write_success:

                        resp_tlv = number_toolkit.compose_down_reply(
                            number_toolkit, msg_id
                        )
                        print(len(resp_tlv), "resp_tlv: ", resp_tlv)
                        # print("resp_tlv another way: ", number_toolkit.str_to_binary(resp_tlv))
                        try:
                            down_rsp = pure_mqtt_pub(sub_topic, resp_tlv, True, qos)
                            print("down_rsp: ", down_rsp)
                        except OSError:
                            print("555 OSError sub_topic")
                break


def mqtt_conn(retry_times):
    global mqtt_connect_state
    global mqtt_client
    # global lock
    global mqtt_con_flag

    print("mqtt link status", mqtt_connect_state)

    if mqtt_connect_state != mqtt_connected:
        print("mqtt conn failed, re-conning ...")

        try:
            mqtt_params = prj_info["mqtt_params"]
            print(
                "## mqtt inti params: ",
                mqtt_params["client_id"],
                mqtt_params["mqtt_url"],
                mqtt_params["mqtt_port"],
                mqtt_params["product_key"],
                mqtt_params["password"],
                mqtt_params["keepalive"],
            )
            mqtt_client = MQTTClient(
                mqtt_params["client_id"],
                mqtt_params["mqtt_url"],
                mqtt_params["mqtt_port"],
                mqtt_params["product_key"],
                mqtt_params["password"],
                mqtt_params["keepalive"],
            )
            mqtt_client.set_callback(sub_cb)
            mqtt_connect_state = mqtt_client.connect()
            if mqtt_connect_state != mqtt_connected:

                print(
                    "^^ mqtt re-connected failed", mqtt_connect_state, utime.localtime()
                )
                utime.sleep(10)
                if retry_times > 0:
                    mqtt_conn(retry_times - 1)
                else:
                    print("mqtt conn failed, rebooting...")
                    utime.sleep(2)
                    system.reset()

            else:
                print("@@ ----mqtt connect successful----", utime.localtime())

                mqtt_client.subscribe(
                    "$sys/"
                    + mqtt_params["product_key"]
                    + "/"
                    + mqtt_params["client_id"]
                    + "/thing/tlv/down/+"
                )
                # print("topic_sub_down ", topic_sub_down, type(topic_sub_down))
                if not if_looping:
                    # 创建mqtt接收数据任务，1.5s一次
                    _thread.start_new_thread(mqtt_loop, ())

                """
                mqtt_client.subscribe(
                    "$sys/"
                    + prj_info["mqtt_params"]["product_key"]
                    + "/"
                    + prj_info["mqtt_params"]["client_id"]
                    + "/thing/tlv/up/accepted"
                )
                mqtt_client.subscribe(
                    "$sys/"
                    + prj_info["mqtt_params"]["product_key"]
                    + "/"
                    + prj_info["mqtt_params"]["client_id"]
                    + "/thing/tlv/up/rejected"
                )
                mqtt_client.subscribe(
                    "$sys/"
                    + prj_info["mqtt_params"]["product_key"]
                    + "/"
                    + prj_info["mqtt_params"]["client_id"]
                    + "/thing/tlv/down_reply/+/accepted"
                )
                mqtt_client.subscribe(
                    "$sys/"
                    + prj_info["mqtt_params"]["product_key"]
                    + "/"
                    + prj_info["mqtt_params"]["client_id"]
                    + "/thing/tlv/down_reply/+/rejected"
                )
                """

        except:
            print(
                "^^ error occurred during MQTT connection",
                mqtt_connect_state,
                utime.localtime(),
            )

            utime.sleep(10)
            if retry_times > 0:
                mqtt_conn(retry_times - 1)
            else:
                print("^^ except: mqtt conn failed, rebooting")
                system.reset()


# mqtt数据接收监测
def mqtt_loop():
    global mqtt_client
    global if_looping
    n = 0
    while True:
        if_looping = True
        utime.sleep_ms(1500)
        try:
            n += 1
            mqtt_client.check_msg()
            # print(n)
            if n >= 10:
                mqtt_client.ping()
                n = 0
                print("ping")
        except:
            print("mqtt disconnect!")
            mqtt_client.disconnect()
            if_looping = False
            mqtt_conn(5)


def period_mqtt_publish(trigger):
    print("timely_publish", trigger)
    global publish_ts
    global mqtt_connect_state
    if mqtt_client is not None:
        mqtt_conn(5)
    for job in prj_info["mqtt_params"]["devices"]:
        print("job: ", job["subject"], job["tlv"])

        if job["tlv"] is None:
            continue

        if mqtt_connect_state != mqtt_connected:
            mqtt_conn()
        publish_result = pure_mqtt_pub(
            "$sys/"
            + prj_info["mqtt_params"]["product_key"]
            + "/"
            + prj_info["mqtt_params"]["client_id"]
            + "/thing/tlv/up",
            # len(job["tlv"]),
            job["tlv"],
            True,
            qos,
        )
        print(
            "publish_result: ",
            publish_result,
            job["subject"],
            job["tlv"],
        )
        utime.sleep(1)


def pure_mqtt_pub(topic, msg, if_retain, qos):
    global mqtt_connect_state
    global publish_ts
    if mqtt_client is None:
        mqtt_conn(5)
    try:
        publish_result = mqtt_client.publish(
            topic,
            msg,
            if_retain,
            qos,
        )
        # utime.sleep(1)
        # mqtt_client.wait_msg()
        print("692 publish_result: ", topic, msg)
        if not publish_result:
            print("^^ publish failed, try reconnecting... ")
            mqtt_connect_state = 2
            mqtt_client.disconnect()
            utime.sleep(1)
            mqtt_conn(5)
        else:
            mqtt_connect_state = 0
            publish_ts = utime.time()
            return True

    except OSError:
        print("^^ timely_publish OSError")

        mqtt_connect_state = 2
        mqtt_client.disconnect()
        utime.sleep(1)
        mqtt_conn(5)


def xaiot_http(
    product_id,
    func_params,
    access_token,
):
    global http_client
    header = (
        "pid:"
        + product_id
        + "\r\ndev_token:%s\r\nContent-Type:application/json\r\n" % (access_token)
    )
    body = (
        '{"version": "1.0","id": 1,"method": "notify","data": {"params":'
        + str(func_params)
        + "}}"
    )
    # print(device_title +' header',header)
    print("http body", body, print(utime.localtime()))
    http_url = "https://https.xaiotjr.cn:8543/thing/json/up"
    http_client.set_header(header)
    http_client.set_data(body, len(body))
    ret = http_client.post(http_url)
    if ret < 0:
        print("^^ http post return failed %d" % ret)
        return None
    responsecode = http_client.get_response_code()

    if responsecode == 200:
        print("@@ https success")
        return True
    else:
        print("^^ https fail")
        return False


def detail_modbus_job(com_params):
    result_value = None
    if com_params["io_type"] in ["haas_di", "haas_relay"]:
        try:
            the_io = GPIO()
            the_io.open(com_params["com_board_item"])
            result_value = the_io.read()
            the_io.close()
            if result_value in [0, 1]:
                return result_value
            else:
                return None
        except:
            print("^^ GPIO error: ", com_params["com_board_item"])
            return None

    if com_params["io_type"] in ["485", "232"]:
        # 判断是haas板载的还是rs接口
        try:
            if_modbus_open = modbus_engine.init(com_params["com_board_item"])
            print("if_modbus_open", if_modbus_open, com_params["com_board_item"])

            result_value = getattr(modbus_toolkit(), com_params["func_title"])(
                modbus_engine,
                com_params,
                5,
            )
            if_modbus_close = modbus_engine.deinit()
            print("if_modbus_close", if_modbus_close)
            return result_value
        except:
            print(
                "^^ 485 232 error: ",
                com_params["com_board_item"],
                com_params["device_addr"],
            )
            return None


def modbus_job():
    global prj_info
    print("modbus_job")
    # modbus_dvs_list = []

    if (
        "mqtt_params" in prj_info
        and "devices" in prj_info["mqtt_params"]
        and len(prj_info["mqtt_params"]["devices"])
    ):
        devices = prj_info["mqtt_params"]["devices"]

        for device in devices:
            print("modbus job: ", ujson.dumps(device["subject"]))
            if "com_params" in device:
                result_value = detail_modbus_job(device["com_params"])
                if result_value is not None:
                    # 组建tlv
                    device["tlv"] = number_toolkit.composeTlV(
                        number_toolkit,
                        device["index"],
                        device["type"],
                        result_value,
                    )
                else:
                    device["tlv"] = None
    else:
        print("^^ unable to read modbus")

    utime.sleep(30)


def sntp_time_sync(now_local_time):
    global sntp_time_sync_date
    if (
        now_local_time[3] == 6
        and now_local_time[4] == 6
        and sntp_time_sync_date
        != [now_local_time[0], now_local_time[1], now_local_time[2]]
    ):
        if sntp.settime() == 1:
            sntp_time_sync_date = [
                now_local_time[0],
                now_local_time[1],
                now_local_time[2],
            ]
            print("@@ getting sntp time succ")
        else:
            print("## getting sntp time fail")


def check_ota_time(now_local_time):
    # global machine_info
    # 如果未检查而且时刻匹配，则检查
    if (
        not machine_info["if_checked_ota"]
        and now_local_time[3:4] == machine_info["imei_ota_time"][0:1]
    ):
        check_ota()


def main_loop():
    while True:
        modbus_job()
        period_mqtt_publish("timely_publish")
        utime.sleep(40)


if __name__ == "__main__":

    print("--- Xiongan Public IoT Platform Netgate---")
    print("---Hebei Xiongan Simplewater Internet Technology Company---")
    print("---Wang Jiancun 18510412016---")
    now_local_time = utime.localtime()
    print("mathine localtime: ", now_local_time)

    # 获取机器imei
    check_machine()

    # 连接网络
    connect_network(10)

    # 前往http节点，获取配置信息
    # 若本地有配置信息，则直接使用，无须再联网
    get_prj_info(3)

    # 建立雄安云连接
    mqtt_conn(5)

    # 把定时任务列到新线程里
    _thread.start_new_thread(main_loop, ())

    # 定时上报
    while True:
        now_local_time = utime.localtime()
        print("cycle time: ", now_local_time)

        # 每天6点6分更新sntp时间
        sntp_time_sync(now_local_time)

        # 每天根据自己的imei标签时刻，检查ota
        check_ota_time(now_local_time)

        utime.sleep(20)
